package nl.ViewerServer.ServerManager.Paraview;

import java.io.File;
import java.util.ArrayList;
import java.util.HashMap;

import nl.ViewerServer.Application;
import nl.ViewerServer.Exceptions.ParameterException;
import nl.ViewerServer.Exceptions.ParaviewConnectionException;
import nl.ViewerServer.Exceptions.PythonException;
import nl.ViewerServer.ServerManager.ServerManager;
import nl.ViewerServer.ServerManager.DataManager.Objects.Parameter;
import nl.ViewerServer.ServerManager.JobManager.ParaViewThreadAssignment;
import nl.ViewerServer.SessionManager.Objects.Job;
import nl.ViewerServer.SessionManager.Objects.Session;

import org.apache.log4j.Logger;

public class Worker extends Thread {
   private static Logger            log                      = Logger.getLogger( Worker.class );
   private static int               workerIdIncrement        = 1;
   private ParaviewInterface        paraview                 = null;
   public final int                 workerId;
   protected Session                session;

   private boolean                  running                  = true;
   private boolean                  shuttingDown             = false;
   protected Throwable              lastError                = null;

   private ParaViewThreadAssignment paraviewThreadAssignment = null;

   /**
    * This parameter may be used to enable checking the timeout in the worker thread, instead of closing the worker
    * immediately when the work is done (aka no jobs available anymore)
    */

   private boolean                  timeoutDisabled          = false;

   public Worker( Session session ) {
      this.workerId = Worker.workerIdIncrement++;
      this.session = session;
   }

   public boolean initiateShutdown( long optionalTimeout ) {
      shuttingDown = true;
      if( optionalTimeout != 0 ) {
         long timeout = System.currentTimeMillis() + optionalTimeout;
         while( running && System.currentTimeMillis() < timeout ) {
            try {
               Thread.sleep( 1 );
            }
            catch( InterruptedException ie ) {
            };
         }
         return !running;
      }
      return true;
   }

   public void asynchronousShutdown( long optionalTimeout ) {
      Thread shutdownThread = new Thread() {
         public void run() {
            shutdown();
            log.info( "Worker " + this.getId() + " was shut down" );
         }
      };
      shutdownThread.start();
      try {
         if( optionalTimeout != 0 ) // using 0 for parameter below means waiting forever, but we don't want to wait at
            // all
            shutdownThread.join( optionalTimeout );

      }
      catch( InterruptedException ie ) {
         // ignore
      }
      return;
   }

   public void shutdown() {
      if( shuttingDown && paraview == null )
         return;

      initiateShutdown( 1000 );
      log.debug( "Shutting down Paraview" );
      paraviewShutdown();
   }

   private void paraviewShutdown() {
      if( paraview != null ) {
         paraview.shutdown();
         paraview = null;
      }
   }

   public void setTimeoutDisabled( boolean timeoutDisabled ) {
      this.timeoutDisabled = timeoutDisabled;
   }

   public void run() {
      log.debug( "Worker " + workerId + " started" );
      if( session.getJobsWaiting() == 0 ) {
         log.info( "Worker " + workerId + " ended because the session does not seem to have jobs anymore" );
         session.setStatus( Session.Status.IDLE );
         return;
      }

      session.setStatus( Session.Status.WORKING );

      try {
         try {
            initializePipeline();
         }
         catch( ParaviewConnectionException pce ) {
            lastError = pce;
            // we should try to reconnect on this error, don't set the session to failed yet (unless we've had too many
            // retries, see jobManager.afterExecute and Application.config.sessionRetryCount)
            session.incrementRetryCount();
            return; // finally-code below will be executed before the actual return!
         }
         catch( PythonException pe ) {
            lastError = pe;
            // this is a fatal error, this means some of the python in the code of the pipeline elements
            // was incorrect
            log.error( "Pipeline initialization failed because of a Python error: ", pe );
            session.setStatusFailed( "A script error occurred while setting up the visualization" );
            return; // finally-code below will be executed before the actual return!
         }
         catch( ParameterException pe ) {
            // probably aligned modality parameters could not be parsed after getting them from Python
            // this is a severe error, but not a fatal error
            // lastError = pe;
            log.error( "A parameter could not be retrieved while initializing the pipeline: ", pe );
            // don't return because we don't consider this error to be fatal (for now!)
         }

         // pipeline initialization succeeded, check for work

         try {
            long lastAction = System.currentTimeMillis();
            while( !shuttingDown && ( System.currentTimeMillis() - lastAction ) < Application.config.sessionWorkerTimeout ) {
               // check for work
               Job j = session.getNextJob();
               if( j != null ) {
                  log.debug( "Worker " + workerId + " doing job " + j );
                  doJob( j );

                  lastAction = System.currentTimeMillis();
               } else {
                  // when the timeout has been disabled (happens when the worker goes into batch mode) the worker will
                  // be closed immediately when there are no more jobs available
                  if( timeoutDisabled ) {
                     shuttingDown = true;
                  }
                  try {
                     Thread.sleep( 1 );
                  }
                  catch( InterruptedException ie ) {
                     log.debug( "Worker " + workerId + " woken up" );
                  }
               }
            }
         }
         catch( ParaviewConnectionException pce ) {
            // Some problem with the paraview connection (often segfault in Paraview)
            // Fail the session!
            if( paraview == null || paraview.isShuttingDown() ) {
               log.error( "Paraview Connection error: ", pce );
            } else {
               log.error( "An unknown Paraview error occurred (often segfault)" );
            }
            lastError = pce;
            session.setStatusFailed( "A Paraview error occurred, restart the visualization" );
         }
         catch( Throwable e ) { // throwable here because we don't want to have any worker take the gateway down
            log.error( "Unexpected exception while in worker", e );
            lastError = e;
            session.incrementRetryCount();
         }
      }
      finally {
         // set status to IDLE (if not already closed or failed status)
         session.setStatus( Session.Status.IDLE );

         running = false;

         // force shutdown (also of Paraview)
         shutdown();

         ServerManager.jobManager.reclaimParaviewThreadAssignment( paraviewThreadAssignment );
      }

      // NB. After this, the jobManager.afterExecute method is called which may choose to re-submit the session
      // to be picked up by another worker

   }

   private void doJob( Job job ) throws Throwable {
      String storeLocation = null;
      try {
         session.setStatus( Session.Status.WORKING );

         storeLocation = ServerManager.dataManager.getStoreLocation( job.jobId );

         Parameter p = session.getParameter( "StoreLocation", null );
         p.setParameterValue( storeLocation );

         long start = System.currentTimeMillis();

         // set output first (as that may affect adjustable parameters)
         session.setOutput( job.output );

         // setParameters method will also set all session parameters (currently: StoreLocation),
         // and annotationParameters (currently: AlignedSourcesVisible)
         setParameters( job.inParameterNames, job.inJavaObjectParameterValues );

         // TODO: try to find the requested visualization result in the cache or database
         // For now, implementation of this feature has been postponed: there is no reliable way of guaranteeing
         // that the scripts in the visualization pipeline don't use internal variables for their visualization

         session.setProgress( "Rendering", 0 );

         executePipelineOutput();

         session.setProgress( "Rendering", 100 );

         getParameters( session.getAllGettableParameters() );

         // check if the storeLocation was changed by the pipeline output
         if( !storeLocation.equals( (String)( session.getParameter( "StoreLocation", null ).getParameterValue() ) ) ) {
            // try to remove the location
            if( !new File( storeLocation ).delete() ) {
               log.warn( "StoreLocation was altered by output script, but the server-proposed StoreLocation could not be removed" );
            } else {
               log.info( "StoreLocation was altered by output script, the original directory was removed" );
            }
         }

         log.info( "Storing output, storeLocation = " + session.getParameter( "StoreLocation", null ).getPythonRepresentation() );
         ServerManager.dataManager.storeOutput( job, session );

         long end = System.currentTimeMillis();

         log.info( "Worker done in " + ( end - start ) + " ms" );

         job.setStatus( Job.Status.FINISHED );
         // save in the Job object how many threads have worked on this job
         job.setPvThreads( paraviewThreadAssignment.totalThreadCount );

      }
      catch( Throwable t ) {
         job.setStatus( Job.Status.FAILED, "<No failure reason specified yet>" );
         // if a store location was provided, remove it
         if( storeLocation != null )
            ServerManager.dataManager.withdrawStoreLocation( storeLocation );

         throw t;
      }
      finally {
         session.jobAltered( job );
      }
   }

   /**
    * This method will setup a worker (and its underlying Paraview process) with all pipeline elements specified by a
    * given session
    * 
    * Also: check the shuttingDown flag before each (possibly time-consuming) step in the initialization process
    * 
    * @throws ParaviewConnectionException
    * @throws PythonException
    * @throws ParameterException
    *            when any of the annotation parameters cannot be parsed from the server
    */

   public void initializePipeline() throws ParaviewConnectionException, PythonException, ParameterException {
      if( paraview != null )
         return;

      long start = System.currentTimeMillis();

      paraviewThreadAssignment = ServerManager.jobManager.assignParaviewThreads( session.requestedThreads );

      if( paraviewThreadAssignment == null )
         throw new ParaviewConnectionException( "No more Paraview threads available" );

      paraview = new ParaviewInterface( workerId, paraviewThreadAssignment );

      if( shuttingDown )
         throw new ParaviewConnectionException( "Aborting initialization, shutdown requested" );
      session.setProgress( "Setting up session", 0 );
      paraview.setupSession( session.sessionId, false );

      if( shuttingDown )
         throw new ParaviewConnectionException( "Aborting initialization, shutdown requested" );
      session.setProgress( "Reading modality", 0 );
      paraview.setupSource( session.getPipelineSource().parseScript(), true );

      if( session.getAlignedModalityPipelineSources() != null ) {
         // if the above call returns null, the 'requiresAlignedSources' flag in db for the chosen visualization is
         // false/NULL

         if( shuttingDown )
            throw new ParaviewConnectionException( "Aborting initialization, shutdown requested" );
         session.setProgress( "Setting up aligned modalities", 0 );

         for( int i = 0; i < session.getAlignedModalityPipelineSources().length; i++ ) {
            paraview.setupAlignedModalitySource( session.getAlignedModalityPipelineSources()[i].parseScript(),
                  session.getAlignedModalityPipelineSources()[i].name, true );
         }

         getParameters( session.getAlignedModalityParametersWithGetters() );
      }

      if( shuttingDown )
         throw new ParaviewConnectionException( "Aborting initialization, shutdown requested" );
      session.setProgress( "Setting up pipeline", 0 );
      paraview.setupPipeline( session.getPipeline().parseScript(), true );

      long end = System.currentTimeMillis();
      log.debug( "Paraview connection + pipeline setup took " + ( end - start ) + " ms" );
   }

   public String executePipelineOutput() throws ParaviewConnectionException, PythonException {
      if( session.getSelectedOutput() == null )
         throw new PythonException( "No output selected" );

      String script = session.getSelectedOutput().parseScript();

      return paraview.executePipelineOutput( script, true );
   }

   public void setParameters( String[] parameterNames, Object[] parameterValues ) throws ParaviewConnectionException,
         PythonException, ParameterException {
      ArrayList<Parameter> parametersToSet = new ArrayList<Parameter>();
      if( parameterNames != null && parameterValues != null ) {
         for( int i = 0; i < parameterNames.length; i++ ) {
            String pName = parameterNames[i];
            Parameter p = session.getParameter( pName, null );
            if( p == null ) {
               log.warn( "setParameters: cannot find parameter " + pName + " in session" );
            } else {
               if( p.hasSetter() == false || p.isUserModifiable() == false || p.isSessionParameter() ) {
                  log.warn( "setParameters: parameter " + pName + " has no setter available or is not allowed to be modified!" );
               } else {
                  try {
                     // log.info( "Parameter: " + parameterNames[i] + " is: [" + parameterValues[i] + "] of type "
                     // + parameterValues[i].getClass() );

                     p.setUserParameterValue( parameterValues[i] );
                     // p.setInUseByPython( true );
                     parametersToSet.add( p );
                  }
                  catch( ParameterException pe ) {
                     log.warn( "Client parameter " + pName + " parse error: " + parameterValues[i] + ", ignored (" + pe + ")" );
                  }
               }
            }
         }
      }

      for( Parameter p : session.getSessionParameters().values() ) {
         if( p.hasSetter() ) {
            parametersToSet.add( p );
         }
      }
      for( Parameter p : session.getAlignedModalityParameters().values() ) {
         if( p.hasSetter() ) {
            parametersToSet.add( p );
         }
      }

      paraview.setParameters( parametersToSet, true );
   }

   public void getParameters( HashMap<String, Parameter> parameters ) throws ParaviewConnectionException,
         PythonException, ParameterException {
      if( parameters == null )
         return;

      String[] parameterNames = new String[ parameters.size() ];
      int i = 0;
      for( String pName : parameters.keySet() ) {
         parameterNames[i++] = pName;
      }
      getParameters( parameterNames );
   }

   public void getParameters( String[] parameterNames ) throws ParaviewConnectionException, PythonException,
         ParameterException {
      if( parameterNames == null || parameterNames.length == 0 )
         return;

      ArrayList<Parameter> parametersToGet = new ArrayList<Parameter>();
      for( int i = 0; i < parameterNames.length; i++ ) {
         String pName = parameterNames[i];
         Parameter p = session.getParameter( pName, null );
         if( p == null ) {
            log.warn( "getParameters: cannot find parameter " + pName + " in session" );
         } else {
            if( p.hasGetter() == false ) {
               log.warn( "getParameters: parameter " + pName + " has no getter available" );
            } else {
               parametersToGet.add( p );
            }
         }
      }

      if( parametersToGet.size() == 0 )
         return;

      paraview.updateParameters( parametersToGet, true );
   }

   public Session getSession() {
      return session;
   }

   public Throwable getLastError() {
      return lastError;
   }

}
